using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;

//using java.nio;
//using java.nio.channels;
//using java.util.concurrent;
using Stunt.Nutss;
using Stunt.Nutss.Signaling;
using Stunt.Nutss.Stunt;

namespace Stunt.Nutss.Stunt
{

	#region STUNTEndPointType

	internal enum STUNTEndPointType
	{
		PROTO_TCP,
		PROTO_UDP
	}

	#endregion

	#region STUNTEndpoint

	/// <summary>
	/// Implements a STUNT TCP endpoint. The same class is used for both TCP server sockets and
	/// TCP client sockets. accept() and connect() operations are asyncronous and reported via
	/// a callback mechanism. Instead of IP addresses and ports, the user-visible address is a
	/// Uri which can be unique even in the presence of NATs.
	/// </summary>
	public class STUNTEndpoint
	{

		#region Variables

		/// <summary>Real worker thread</summary>
		private Thread workerThread;

		/// <summary>Real worker</summary>
		private Worker worker;

		#endregion

		#region Constructor

		/// <summary>
		/// Creates a new STUNT endpoint bound to the local user and hostname.
		/// </summary>
		public STUNTEndpoint()
			: this(null)
		{
		}

		/// <summary>
		/// Creates a new STUNT endpoint bound to the given Uri. If id is null, binds
		/// to a random name based on the local username and hostname.
		/// </summary>
		/// <param name="id">the local address</param>
		public STUNTEndpoint(NutssUri id)
		{

			try
			{
				/*
				 * System.getenv("user") gets the user's loging name as configured into
				 * environment variables. this is usuaully only on unix type systems
				 */

				if (id == null)
				{
					//id = new Uri(System.getenv("USER") + ((int)(new Random().NextDouble() * 1024)) + "@" + Dns.GetHostName());
					id = new NutssUri(Guid.NewGuid().ToString() + "@" + Dns.GetHostName());
				}
				worker = new Worker(new SlingProvider(), id);
				workerThread = new Thread(new ThreadStart(worker.ThreadProcess));
				workerThread.IsBackground = true;
				workerThread.Start();
				// Wait for the worker to signal us whether or not the
				// proxy communication worked out.
				Debug.WriteLine("Acquiring semaphore");
				worker.semaphore.WaitOne();
				Debug.WriteLine("Acquired semaphore");

                if (worker.Reason != null)
					throw worker.Reason;
			}

			catch (Exception)
			{

				if (workerThread != null)
					workerThread.Interrupt();
			}
		}

		#endregion

		#region Listen

		/// <summary>
		/// Marks this endpoint as willing to receive incoming connections
		/// </summary>
		/// <param name="cb">the callback functions to call for Accept()</param>
		public void Listen(ISTUNTEventListener cb)
		{
			worker.Listen(cb);
		}

		#endregion

		#region Connect - TCP - UDP

		/// <summary>
		/// Initiate an outgoing connection
		/// </summary>
		/// <param name="dst">destination ID</param>
		/// <param name="cb">callback for success or failure</param>
		/// <deprecated>Please now use connectTCP() or connectUDP()</deprecated>
		public void Connect(NutssUri dst, ISTUNTEventListener cb)
		{
			ConnectTCP(dst, cb);
		}

		/// <summary>
		/// Initiate an outgoing TCP connection
		/// </summary>
		/// <param name="dst">destination ID</param>
		/// <param name="cb">callback for success or failure</param>
		public void ConnectTCP(NutssUri dst, ISTUNTEventListener cb)
		{
			worker.Connect(dst, cb, STUNTEndPointType.PROTO_TCP);
		}

		/// <summary>
		/// Initiate an outgoing UDP connection
		/// </summary>
		/// <param name="dst">destination ID</param>
		/// <param name="cb">callback for success or failure</param>
		public void ConnectUDP(NutssUri dst, ISTUNTEventListener cb)
		{
			worker.Connect(dst, cb, STUNTEndPointType.PROTO_UDP);
		}

		#endregion

		#region Close

		/// <summary>
		/// Closes the endpoint.
		/// </summary>
		public void Close()
		{
			workerThread.Interrupt();
		}

		#endregion

	}

	#endregion

	#region Worker

	/// <summary>
	/// The real class that implements the STUNT endpoint. Encapsulated to hide public
	/// methods of this class from access by user functions.
	/// </summary>
	internal class Worker : Dispatch
	{

		#region Inner class: KeepAliveSTUNTEventAdapter

		internal class KeepAliveSTUNTEventAdapter : STUNTEventAdapter
		{
			Worker _parent;

			internal KeepAliveSTUNTEventAdapter(Worker parent)
			{
				_parent = parent;
			}
			/// continuation terminated, we will soon lose our binding.
			/// Terminate the endpoint as a result.
			override public void ErrorHandler(Exception e)
			{
				_parent.Reason = e;
				Thread.CurrentThread.Interrupt();
			}
		}

		#endregion

		#region Variables

		/// <summary>
		/// Used for synchronizing between the thread creating the endpoint
		/// and the thread performing the equivalent of the bind operation.
		/// </summary>
		internal Semaphore semaphore;

		/// <summary>
		/// local endpoint's Uri
		/// </summary>
		private NutssUri id;

		/// <summary>
		/// STUNT servers learned from the signaling infrastructure
		/// </summary>
		private IPEndPoint[] servers;

		/// <summary>
		/// Tracks the various running continuation/event handlers and the callback
		/// object associated with them.
		/// </summary>
		private Dictionary<IContinuation, ISTUNTEventListener> passive, active, keepalive;

		/// <summary>
		/// Stores the reason why the whole STUNT endpoint was closed. This is different
		/// from intermediate exceptions generated as and when connect() operations fail.
		/// </summary>
		private Exception reason;

		/// <summary>
		/// Whether the endpoint is closed or not
		/// </summary>
		private bool closed;

		/// <summary>
		/// Stores the master callback for endpoints in listen mode. Each time
		/// a new connection attemp arrives, a mini continuation is created and
		/// associated with a copy of this callback object.
		/// </summary>
		private ISTUNTEventListener listenCB;

		#endregion

		#region Constructor

		/// <summary>
		/// Creates a new worker bound to the given signaling context and local address
		/// </summary>
		/// <param name="context">signaling channel</param>
		/// <param name="id">local ID</param>
		internal Worker(ISignalingContext context, NutssUri id)
			: base(context)
		{
			this.id = id;
			semaphore = new Semaphore(0, 1000);
			passive = new Dictionary<IContinuation, ISTUNTEventListener>();
			active = new Dictionary<IContinuation, ISTUNTEventListener>();
			keepalive = new Dictionary<IContinuation, ISTUNTEventListener>();
		}

		#endregion

		#region Callback

		override public void Callback(IContinuation k, object o)
		{
			Socket socket = o as Socket;
			ISTUNTEventListener cb;

			if (passive.ContainsKey(k))
			{
				cb = passive[k];

				// an incoming connect() attempt either succeeded or failed.
				// If it succeeded, notify the callback
				// If it failed, the callback doesn't care either way
				// In any case, the continuation that was performing the accept
				// is dead.
				if (socket != null)
				{

					if (socket.ProtocolType == ProtocolType.Tcp)
						cb.AcceptTCPHandler(socket, ((STUNTCont)k).dst);

					else if (socket.ProtocolType == ProtocolType.Udp)
						throw new NotImplementedException();
					//cb.AcceptUDPHandler(socket, ((STUNCont)k).dst);
				}
				passive.Remove(k);
			}

			else if (active.ContainsKey(k))
			{
				cb = active[k];

				// an outgoing connect() succeeded or failed. Notify the callback
				// in both cases. Reap the deal continuation.
				if (socket != null)
				{

					if (socket.ProtocolType == ProtocolType.Tcp)
						cb.ConnectTCPHandler(socket);

					else if (socket.ProtocolType == ProtocolType.Udp)
						cb.ConnectUDPHandler(socket);
				}

				else if (o is Exception)
					cb.ErrorHandler((Exception)o);
				active.Remove(k);
			}

			else if (keepalive.ContainsKey(k))
			{
				cb = keepalive[k];

				// keepalives are signaling context specific. Can't assume much about
				// them. If they give us some STUNT server addresses, use them. If there
				// is an error, reap them. Also, if we got server addresses, we bind() to
				// the given Uri succeeded -- inform the creater thread if it is still waiting.
				if (o is IPEndPoint[])
				{
					servers = (IPEndPoint[])o;
					semaphore.Release();
				}

				else if (o is Exception)
				{
					cb.ErrorHandler((Exception)o);
					keepalive.Remove(k);
				}
			}

			else return;
		}

		#endregion

		#region CheckClosed

		/// <summary>
		/// Checks whether the endpoint has already been closed. This should be called
		/// with the synchronization lock on this object held.
		/// </summary>
		private void CheckClosed()
		{

			if (closed)
			{
				Exception e = new Exception("Endpoint closed.", reason);
				Debug.WriteLine("Worker - CheckClosed" + e.Message);
				throw e;
			}
		}

		#endregion

		#region Listen

		/// <summary>Marks this endpoint as willing to receive incoming connections</summary>
		/// <param name="cb">the callback functions to call for accept</param>
		internal void Listen(ISTUNTEventListener cb)
		{
			lock (this)
			{
				CheckClosed();

				if (listenCB != null)
					throw new Exception("Already listening.");
				listenCB = cb;
			}
		}

		#endregion

		#region Reason

		/// <summary>Sets the reason why the endpoint was closed.</summary>
		internal Exception Reason
		{
			get
			{
				return reason;
			}
			set
			{

				if (reason == null)
					reason = new Exception("Endpoint closed.", value);
			}
		}

		#endregion

		#region HandleMessage

		override public void HandleMessage(ISignalingMessage msg)
		{
			lock (this)
			{

				// Make sure the proxy message is an INVITE and not a stray
				// error response for an old invite or something.
				if (msg.HasPayload &&
					msg.Payload is STUNTMessage &&
					!msg.Error &&
					listenCB != null)
				{

					if (((STUNTMessage)msg.Payload).stage == STUNTMessage.Stage.STUNT_INVITE)
					{
						// If user/app confimation is required before attempting to establish
						// a connection, this would be a good place to check. The listenCB
						// that was passed from the app earlier can have an 'bool allowURI(Uri remote)'
						// function.
						IContinuation k = new STUNTCont(msg.Source, msg.Destination, sig, servers);
						passive.Add(k, listenCB);
						AddContinuation(k, msg);
					}

					if (((STUNTMessage)msg.Payload).stage == STUNTMessage.Stage.STUN_INVITE)
					{
						throw new NotImplementedException();
						/*

						try
						{
							// If user/app confimation is required before attempting to establish
							// a connection, this would be a good place to check. The listenCB
							// that was passed from the app earlier can have an 'bool allowURI(Uri remote)'
							// function.
							//Continuation k = new STUNCont(msg.Source, msg.Destination, sig, servers, null);
							//passive.Add(k, listenCB);
							//AddContinuation(k, msg);
						}

						catch (Exception) { }
						*/
					}
				}
			}
		}

		#endregion

		#region Connect

		/// <summary>Initiate an outgoing connection</summary>
		/// <param name="dst">destination ID</param>
		/// <param name="cb">callback for success or failure</param>
		internal void Connect(NutssUri dst, ISTUNTEventListener cb, STUNTEndPointType t)
		{
			lock (this)
			{
				CheckClosed();
				IContinuation k = null;

				switch (t)
				{

					case STUNTEndPointType.PROTO_TCP:
						k = new STUNTCont(dst, id, sig, servers);
						break;

					case STUNTEndPointType.PROTO_UDP:
						//k = new STUNCont(dst, id, sig, servers, null);
						//break;
						throw new NotImplementedException();
					default:
						throw new ExtendedSocketException("Protocol not supported");
				}
				active.Add(k, cb);
				AddContinuation(k, null);
			}
		}

		#endregion

		#region ThreadProcess

		override public void ThreadProcess()
		{

			try
			{
				// First start a KeepAlive continuation as a result of
				// registering/binding the ID of the local endpoint with
				// the proxy infrastructure
				IContinuation k = sig.Register(id);

				if (k != null)
				{
					keepalive.Add(k, new KeepAliveSTUNTEventAdapter(this));
					AddContinuation(k, null);
				}
				// Start processing events. If this returns, endpoint is dead.
				base.ThreadProcess();
				// Should never reach here under normal operations. This
				// signifies that all continuations (even keepalive!) have
				// exited without reporting an error.
				Debug.WriteLine("No more continuations");
				Reason = new Exception("All continuations exited");
			}

			catch (Exception e)
			{
				Debug.WriteLine("Worker - ThreadProcess" + e.Message);
				Reason = e;
			}
			lock (this)
			{
				// be sure to report endpoint death to all callbacks
				// avoid race conditions.
				closed = true;
				Reason = new ThreadInterruptedException();

				foreach (ISTUNTEventListener l in passive.Values)
					l.ErrorHandler(reason);

				foreach (ISTUNTEventListener l in active.Values)
					l.ErrorHandler(reason);
			}
			// the thread that created this worker may still be waiting to get a
			// success or failure report; which will never come since the endpoint is
			// dead. Kick that thread back into action.
			semaphore.Release();
		}

		#endregion

	}

	#endregion
}